package org.haptimap.hcimodules.guiding;

import org.haptimap.hcimodules.HCIModule;
import org.haptimap.hcimodules.util.OrientationModule;
import org.haptimap.hcimodules.util.WayPoint;

import android.app.Activity;
import android.content.Context;
import android.location.Location;
import android.os.Handler;
import android.util.Log;

/**
 * This is a general help class providing some basic methods that can be used to build 
 * other Guide modules. The Guide consists on an extension of the OrientationModule,
 * which provides the location/position and the access to the magnetic compass. The
 * basic calculations of position, bearings and deviation are provided through this class.
 *  
 * Guide is the base class used for some parts of the interaction, and it can be used  
 * for the implementation of other guide components. The current components are: HapticGuide, 
 * AudioGuide and SpeechGuide.
 * 
 * @author Miguel Molina, August 2011
 */
public abstract class Guide extends OrientationModule{

	private static final String TAG = "Guide";
	private static final boolean DEBUG = false;

	/**
	 * Broadcast action: Indicates when the destination is reached
	 * 
	 * <p>Sent when the user arrived to the destination. The default radius
	 * to the POI is 15 meters.
	 */
	public static final String ACTION_DESTINATION_REACHED = "org.haptimap.geiger.action.DESTINATION_REACHED";

	/**
	 * The default value used for interaction and for updating values 
	 */
	public static final int DEFAULT_RATE_INTERVAL = 1000;

	/**
	 * The default value for a poi radius. It is used to validate the distance to a {@link WayPoint}
	 * to indicate whether the user's current position is within the area to the destination. 
	 */
	public static final int DEFAULT_POI_RADIUS = 15;

	/**
	 * Value used for one of the calls generated when calling the listener.
	 * It is used to notify the user that he/she has reached a destination.
	 */
	protected static final int ON_DESTINATION_REACHED = 0x100;

	/**
	 * Value used for one of the calls generated when calling the listener.
	 * Used to notify whether the module was successfully loaded.
	 */
	protected static final int ON_PREPARED = 0x200;

	/**
	 * Value used for one of the calls generated when calling the listener.
	 * The value indicates the frequency for which this module is interacting and updating values.
	 */
	protected static final int ON_RATE_INTERVAL_CHANGED = 0x300;

	//Values about the destination
	protected WayPoint mDestination;
	protected float mDistanceToDestination;
	protected float mNextDirection;
	protected float mDeviation;

	//Handler that takes care of this module's Runnable
	protected Handler mHandler;

	//Whether these values are default or altered.
	protected int mCustomPostDelayValue;
	protected boolean mHasCustomPostDelayValue;
	protected int mPoiRadius;
	protected boolean mHasCustomPoiRadius;

	//Whether this module is running.
	private boolean isRunning;

	/**
	 * Default constructor used to initiate this {@link HCIModule}. The context expected is the one 
	 * obtained by using {@link Activity#getApplicationContext()} If other {@link Context} is used, the 
	 * module might not operate if moving from the current {@link Activity} from where it has been created.
	 * 
	 * @param context The application's context 
	 */
	public Guide(Context context) {
		super(context);
		mHandler = new Handler();
		this.mPoiRadius = DEFAULT_POI_RADIUS;
	}

	/**
	 * Constructor with destination. It is recommended that the Module that extends this class
	 * also is calling its respective onStart()
	 * 
	 * @param context The applications context
	 * @param destination The destination/target
	 */
	public Guide(Context context, WayPoint destination){
		this(context);
		if(destination == null){
			throw new IllegalArgumentException("destination is null");
		}
		this.mDestination = destination;
	}

	/**
	 * Constructor with destination. It is recommended that the Module that extends this class
	 * also is calling its respective onStart()
	 * 
	 * @param context The applications context
	 * @param destinationName The destination's name
	 * @param latitude The latitude in WGS84 decimal format
	 * @param longitude The longitude in WGS84 decimal format
	 */
	public Guide(Context context, String destinationName, String latitude, String longitude){
		this(context);
		if(destinationName == null || latitude == null || longitude == null){
			throw new IllegalArgumentException("destinationName, latitude or longitude is null");
		}
		this.mDestination = new WayPoint(destinationName, latitude, longitude);
	}

	@Override
	public void onStart() {
		super.onStart();
		startHandler();
		if(DEBUG) Log.i(TAG, "onStart()");
	}

	@Override
	public void onPause() {
		super.onPause();
		pauseHandler();
		if(DEBUG) Log.i(TAG, "onPause()");
	}

	@Override
	public void onResume() {
		super.onResume();
		resumeHandler();
		if(DEBUG) Log.i(TAG, "onResume()");
	}

	@Override
	public void onStop() {
		super.onStop();
		stopHandler();
		if(DEBUG) Log.i(TAG, "onStop()");
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		destroyHandler();
		if(DEBUG) Log.i(TAG, "onDestroy()");
	}

	/**
	 * Method used to perform an action.
	 */
	protected abstract void performAction();

	protected abstract void onDestinationReached();

	protected abstract void onRateIntervalChanged(long millis);

	private Runnable mRunnable = new Runnable() {

		public void run() {
			if(updateValues()){
				if(outsidePoiRadius()){
					performAction();
				} else {
					onDestinationReached();
					return;
				}
			}			
			mHandler.postDelayed(this, getRateInterval());
		}
	};

	/**
	 * Check if the current distance to destination is valid according to {@link #DEFAULT_POI_RADIUS}
	 * @return whether the distance is &gt {@link #DEFAULT_POI_RADIUS}
	 */
	private boolean outsidePoiRadius(){
		return (mDistanceToDestination > this.mPoiRadius);
	}

	private void startHandler() {
		if(mHandler != null){
			if(!isRunning){			
				mHandler.post(mRunnable);
				isRunning = true;
			} else {
				Log.w(TAG, "already running...");
			}
		} 
		if(DEBUG) Log.i(TAG, "startHandler()");
	}

	//Pauses the handler and removes callbacks
	private void pauseHandler(){
		if(mHandler != null){
			mHandler.removeCallbacks(mRunnable);
			isRunning = false;
		}
		if(DEBUG) Log.i(TAG, "pauseHandler()");
	}

	private void resumeHandler(){
		if(mHandler != null){
			if(!isRunning){
				mHandler.post(mRunnable);
			} else {
				mHandler.removeCallbacks(mRunnable);
				mHandler.post(mRunnable);
			}
			isRunning = true;
		}
		if(DEBUG) Log.i(TAG, "resumeHandler()");
	}

	//Stops the handler and removes callbacks  
	private void stopHandler() {
		if(mHandler != null){
			mHandler.removeCallbacks(mRunnable);
			isRunning = false;
		}
		mHandler = null;
		if(DEBUG) Log.i(TAG, "stopHandler()");
	}

	private void destroyHandler() {
		if(mHandler != null){
			mHandler.removeCallbacks(mRunnable);
			isRunning = false;
		}
		mHandler = null;
		if(DEBUG) Log.i(TAG, "destroyHandler()");
	}	

	private synchronized boolean updateValues(){
		boolean ans = false;

		if(Float.isNaN(mAzimuth)){
			return ans;
		}

		if(this.mCurrentLocation != null && this.mDestination != null){
			Location destLoc = this.mDestination.getLocation();
			try{
				this.mDistanceToDestination = Math.round(mCurrentLocation.distanceTo(destLoc));
				this.mNextDirection = (360 + mCurrentLocation.bearingTo(destLoc))%360;
				this.mDeviation = calculateDeviation();
				if(DEBUG) Log.e(TAG, "initial bearing(mNextDirection)=" + mNextDirection + " mAzimuth360=" + mAzimuth360 + " mDeviation=" + mDeviation + " mDistanceToDestination=" + mDistanceToDestination);
				ans = true;
			} catch (Exception exc){
				Log.e(TAG, exc.toString());
				ans = false;
			} 
		}
		return ans;
	}

	/**
	 * Creates a deviation based on a projection on the azimuth as a zero point in a
	 * full circle of 360 degrees.
	 * 
	 * The idea is to use our azimuth in such a way that it will indicate where the
	 * next direction is. If we could imagine that we are pointing somewhere, say 
	 * azimuth = 355 degrees. If our poi is placed at 50 degrees, then it means that
	 * the poi is at our right hand and we should go to the right. The deviation in 
	 * this case will be 55, meaning that the poi is at 55 degrees from out azimuth..
	 * we should go to the right.  
	 *
	 * @return the deviation based on where the poi is placed from projecting azimuth to a 
	 * 0 degrees reference.
	 * 	
	 */
	private synchronized float calculateDeviation(){
		float ans = 0f;
		if (mNextDirection > mAzimuth360) {
			ans = (mNextDirection - mAzimuth360);
		}else {
			ans = (mNextDirection + 360 - mAzimuth360);
		}
		return ans;
	}

	/**
	 * @return The recommended rate interval before the next call/update/interaction
	 * to this module
	 */
	public long getRateInterval(){		
		long millis;
		if(!mHasCustomPostDelayValue){
			millis = DEFAULT_RATE_INTERVAL;
		} else {
			millis = mCustomPostDelayValue;
		}
		return millis;		
	}

	/**
	 * The current rate interval value in milliseconds between interactions can be altered.
	 * It is recommended to use at least the {@link #DEFAULT_RATE_INTERVAL}
	 * 
	 * @param rateInterval The value in milliseconds for recurrence of update/interaction.
	 * @return whether it was successfully set
	 * 
	 * @throws IllegalArgumentException if the value is negative.
	 */
	public boolean setRateInterval(int rateInterval){		
		if(rateInterval < 0){
			throw new IllegalArgumentException("value < 0");
		} else {
			if(rateInterval < DEFAULT_RATE_INTERVAL) Log.w(TAG, "value is < " + DEFAULT_RATE_INTERVAL);
			this.mCustomPostDelayValue = rateInterval;
			this.mHasCustomPostDelayValue = true;
			onRateIntervalChanged(this.mCustomPostDelayValue);
			return true;
		}
	}

	/**
	 * Used to restore the default update/interaction value.
	 */
	public void setDefaultRateInterval(){
		this.mHasCustomPostDelayValue = false;
		this.mCustomPostDelayValue = DEFAULT_RATE_INTERVAL;
		onRateIntervalChanged(DEFAULT_RATE_INTERVAL);
	}

	/**
	 * The radius can be altered with a custom value in meters.
	 * The radius should not be too small, since the GPS accuracy might vary and the radius 
	 * might become too small.
	 * 
	 * @param radius The radius in meters for the destination point.
	 * @return whether the value was successfully set.
	 */
	public boolean setCustomPoiRadius(int radius){
		if(radius < 0){
			throw new IllegalArgumentException("radius < 0");
		}
		this.mPoiRadius = radius;
		this.mHasCustomPoiRadius = true;
		return true;
	}

	/**
	 * Used to restore the default radius.
	 */
	public void setDefaultPoiRadius(){
		this.mPoiRadius = DEFAULT_POI_RADIUS;
		this.mHasCustomPoiRadius = false;
	}

	/**
	 * The current destination's/point's radius.
	 * 
	 * @return value for the current radius in meters.
	 */
	public int getCurrentPoiRadius(){
		return new Integer(this.mPoiRadius);
	}

	/**
	 * Set the next destination. This is the point to where the user will be guided.
	 * @param destination A WayPoint
	 * 
	 * @throws NullPointerException
	 * @see {@link WayPoint}
	 */
	public void setNextDestination(WayPoint destination){
		if(destination == null){
			throw new IllegalArgumentException("destination");
		} 
		this.mDestination = destination;
	}

	/**
	 * Set the next destination. This is the point to where the user will be guided.
	 * @param destinationName The name of the destination
	 * @param latitude The latitude in WSG84 decimal format
	 * @param longitude The longitude in WSG84 decimal format
	 * 
	 * @see {@link WayPoint}
	 * @see #setNextDestination(WayPoint)
	 * @see #setNextDestination(String, Location)
	 * @see #setNextDestination(String, double[])
	 */
	public void setNextDestination(String destinationName, String latitude, String longitude) {
		this.mDestination = new WayPoint(destinationName, latitude, longitude);
	}

	/**
	 * Set the next destination. This is the point to where the user will be guided. 
	 * @param destinationName The name of the destination
	 * @param destination The {@link Location} destination
	 * 
	 * @see {@link WayPoint}
	 * @see #setNextDestination(WayPoint)
	 * @see #setNextDestination(String, double[])
	 * @see #setNextDestination(String, String, String)
	 */
	public void setNextDestination(String destinationName, Location destination){
		this.mDestination = new WayPoint(destinationName, destination);
	}

	/**
	 * Set the next destination. This is the point to where the user will be guided.
	 * @param destinationName The name of the destination
	 * @param latlon An array containing the latitude and the longitude as follows:
	 * <p> 
	 * <ul>
	 * <li> <strong>latlon[0]:</strong> The latitude in WGS84 format.
	 * <li> <strong>latlon[1]:</strong> The longitude in WGS84 format.
	 * </ul>
	 * 
	 * @see {@link WayPoint#WayPoint(String, double[])}
	 */
	public void setNextDestination(String destinationName, double[] latlon){
		this.mDestination = new WayPoint(destinationName, latlon);
	}

	/**
	 * Provides information about this module's status.
	 *  
	 * @return whether this module is running
	 * */
	public boolean isRunning(){
		return new Boolean(isRunning);
	}

}
